package logger

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"jgt-wallet/src/util"
	"mime"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/teambition/gear"
)

// 定义日志中使用的键名
const (
	FieldKeyTraceID = "trace_id"
	FieldKeyType    = "type"
	FieldKeyUserID  = "user_id"
)

var defaultOptions = options{
	level:  5,
	format: "text",
}

var internalLogger *Logger
var once sync.Once

type options struct {
	level  int
	format string
}

// Option 定义配置参数
type Option func(o *options)

// SetLevel 设定日志级别(0:panic,1:fatal,2:error,3:warn,4:info,5:debug)
func SetLevel(level int) Option {
	return func(o *options) {
		o.level = level
	}
}

// SetFormat 设定日志格式(text/json)
func SetFormat(format string) Option {
	return func(o *options) {
		o.format = format
	}
}

func logger() *Logger {
	if internalLogger == nil {
		internalLogger = New()
	}
	return internalLogger
}

// Default 获取默认日志实例
func Default() *Logger {
	return logger()
}

// System 系统日志
func System(traceID string, userID ...string) *logrus.Entry {
	return logger().System(traceID, userID...)
}

// Access 访问日志
func Access(traceID string, userID ...string) *logrus.Entry {
	return logger().Access(traceID, userID...)
}

// Operate 操作日志
func Operate(traceID string, userID ...string) *logrus.Entry {
	return logger().Operate(traceID, userID...)
}

// Login 登录(登出)日志
func Login(traceID string, userID string) *logrus.Entry {
	return logger().Login(traceID, userID)
}

// New 创建日志实例
func New(opts ...Option) *Logger {
	once.Do(func() {
		o := defaultOptions
		for _, opt := range opts {
			opt(&o)
		}

		l := logrus.New()
		l.SetLevel(logrus.Level(o.level))
		if o.format == "json" {
			l.Formatter = new(logrus.JSONFormatter)
		}
		internalLogger = &Logger{l}
	})
	return internalLogger
}

// HookFlusher 将缓冲区数据写入日志钩子完成接口
type HookFlusher interface {
	Flush()
}

// Logger 日志管理
type Logger struct {
	*logrus.Logger
}

func (a *Logger) typeEntry(traceID, fieldType string, userID ...string) *logrus.Entry {
	fields := logrus.Fields{
		FieldKeyTraceID: traceID,
		FieldKeyType:    fieldType,
	}
	if len(userID) > 0 {
		fields[FieldKeyUserID] = userID[0]
	}
	return a.WithFields(fields)
}

// System 系统日志
func (a *Logger) System(traceID string, userID ...string) *logrus.Entry {
	return a.typeEntry(traceID, "system", userID...)
}

// Access 访问日志
func (a *Logger) Access(traceID string, userID ...string) *logrus.Entry {
	return a.typeEntry(traceID, "access", userID...)
}

// Operate 操作日志
func (a *Logger) Operate(traceID string, userID ...string) *logrus.Entry {
	return a.typeEntry(traceID, "operate", userID...)
}

// Login 登录(登出)日志
func (a *Logger) Login(traceID string, userID string) *logrus.Entry {
	return a.typeEntry(traceID, "login", userID)
}

// Middleware HTTP访问日志中间件
func Middleware(allowRouters ...string) gear.Middleware {
	return func(ctx *gear.Context) error {
		allow := false
		for _, r := range allowRouters {
			if strings.HasPrefix(ctx.Path, r) {
				allow = true
				break
			}
		}
		if !allow {
			return nil
		}

		start := time.Now()

		fields := logrus.Fields{}
		fields["ip"] = ctx.IP().String()
		fields["method"] = ctx.Method
		fields["url"] = ctx.Req.URL.String()
		fields["proto"] = ctx.Req.Proto
		fields["user_agent"] = ctx.Get(gear.HeaderUserAgent)

		if ctx.Method == http.MethodPost || ctx.Method == http.MethodPut {
			mediaType, _, _ := mime.ParseMediaType(ctx.GetHeader(gear.HeaderContentType))
			if mediaType == gear.MIMEApplicationJSON {
				body, err := ioutil.ReadAll(ctx.Req.Body)
				if err == nil {
					ctx.Req.Body.Close()
					buf := bytes.NewBuffer(body)
					ctx.Req.Body = ioutil.NopCloser(buf)
					fields["content_length"] = ctx.Req.ContentLength
					fields["body"] = string(body)
				}
			}
		}

		ctx.OnEnd(func() {
			fields["time_consuming"] = time.Since(start) / 1e6
			fields["status"] = ctx.Res.Status()
			if l := len(ctx.Res.Body()); l > 0 {
				fields["length"] = l
			}

			if v, err := ctx.Any(util.GetAPPParameterKey()); err == nil && v != nil {
				fields["app_header"] = v
			}

			var userID string
			if v, err := ctx.Any(util.GetUserIDKey()); err == nil && v != nil {
				userID = v.(string)
			}

			var traceID string
			if v, err := ctx.Any(util.GetTraceIDKey()); err == nil && v != nil {
				traceID = v.(string)
			}

			var urlMemo string
			if v, err := ctx.Any(util.GetURLMemoKey()); err == nil && v != nil {
				urlMemo = v.(string)
			}

			memo := ctx.Req.URL.Path
			if urlMemo != "" {
				memo = fmt.Sprintf("%s(%s)", memo, urlMemo)
			}

			logger().Access(traceID, userID).WithFields(fields).Infof(
				"[http] %s - %s - %s",
				memo,
				ctx.Method,
				ctx.IP(),
			)
		})
		return nil
	}
}
